﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;

using NMemcached.Client.Configuration;

namespace NMemcached.Client
{
    /// <summary>
    /// A base class for a connection to a single memcached server
    /// </summary>
    /// <remarks>
    /// <para>
    /// The ServerConnection is a more specialized MemcachedConnection that represents a
    /// connection to a single Memcached server. The connections that communicate on the
    /// text and binary Memcached protocols both inherit from ServerConnection.
    /// </para>
    /// </remarks>
    public abstract class ServerConnection : MemcachedConnection, IDisposable
    {
        private Socket _connection;
        private bool _disposed;

        /// <summary>
        /// Gets or sets the encoding for string data written to and received from the cache
        /// </summary>
        public Encoding PayloadEncoding { get; set; }
        /// <summary>
        /// Gets or sets the encoding for strings used in Memcached commands
        /// </summary>
        public Encoding ProtocolEncoding { get; set; }
        /// <summary>
        /// Gets or sets the number of milliseconds after wich the receive call will time out
        /// </summary>
        public int ReceiveTimeout { get; set; }
        /// <summary>
        /// Gets or sets the number of milliseconds after which the send call will time out
        /// </summary>
        public int SendTimeout { get; set; }
        /// <summary>
        /// Gets or sets a value that determines if data sent to the cache is compressed
        /// </summary>
        public bool UseCompression { get; set; }
        /// <summary>
        /// Gets or sets the ICompressor responsible for compression and decompression of cache content
        /// </summary>
        public ICompressor Compressor { get; set; }
        /// <summary>
        /// Gets or sets the ISerializer responsible for serialization and deserialization of objects written to and read from the cache
        /// </summary>
        public ISerializer Serializer { get; set; }
        /// <summary>
        /// Gets the transport type of the connection
        /// </summary>
        protected TransportType TransportType { get; private set; }
        /// <summary>
        /// Gets the remote end point of the memcached server
        /// </summary>
        protected EndPoint RemoteEndPoint { get; private set; }

        /// <summary>
        /// Initializes a new instance of the ServerConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The remote end point of the memcached server</param>
        protected ServerConnection(TransportType transportType, EndPoint remoteEp)
            : this(transportType, remoteEp, Encoding.UTF8, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the ServerConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The remote end point of the memcached server</param>
        /// <param name="protocolEncoding">The character encoding for string data sent to and read from the server</param>
        protected ServerConnection(TransportType transportType, EndPoint remoteEp, Encoding protocolEncoding)
            : this(transportType, remoteEp, protocolEncoding, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the ServerConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The remote end point of the memcached server</param>
        /// <param name="protocolEncoding">The character encoding for string data sent to and read from the server</param>
        /// <param name="useCompression">Determines if non primitive data is compressed when sent to or decompressed when read from the server</param>
        protected ServerConnection(TransportType transportType, EndPoint remoteEp, Encoding protocolEncoding, bool useCompression)
        {
            //Default the send and receive timeouts (5 seconds)
            this.ReceiveTimeout = 5000;
            this.SendTimeout = 5000;

            this.TransportType = transportType;
            this.RemoteEndPoint = remoteEp;

            this.PayloadEncoding = Encoding.UTF8;
            this.ProtocolEncoding = protocolEncoding;
            this.UseCompression = useCompression;

            this.Compressor = new GZipCompressor();
            this.Serializer = new BinarySerializer();
        }

        /// <summary>
        /// When overridden in a derived class, reads the cache statistics for the server
        /// </summary>
        /// <returns>The server statistics</returns>
        public abstract IDictionary<string, string> Stats();

        /// <summary>
        /// When overridden in a derived class, gets the version numbers of the server
        /// </summary>
        /// <returns>A string representation of the memcached server version number</returns>
        public abstract string Version();

        /// <summary>
        /// Closes the connection to the Memcached server
        /// </summary>
        public override void Close()
        {
            if (this._connection != null)
            {
                if (this._connection.Connected)
                {
                    this._connection.Disconnect(true);
                }
                this._connection.Close();
            }
        }

        /// <summary>
        /// Releases the unmanaged resources used by the ServerConnection
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Returns a string representation of the ServerConnection
        /// </summary>
        /// <returns>A string representation of the ServerConnection</returns>
        public override string ToString()
        {
            return String.Format("{0} - {1}", this.GetType().Name, this.RemoteEndPoint.ToString());
        }

        //Protected Methods

        /// <summary>
        /// Releases the unmanaged resources used by the ServerConnection, and optionally disposes of the managed resources
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to releases only unmanaged resources</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                {
                }

                if (this._connection != null)
                {
                    this._connection.Close();
                }

                this._disposed = true;
            }
        }

        /// <summary>
        /// Gets the number of seconds to allow a cached item to live up to a maximum of 30 days.
        /// </summary>
        /// <param name="expiration">The DateTime in the future when the item is to expire</param>
        /// <returns>The total number of seconds up to a maximum of 30 days</returns>
        protected static uint GetExpirationTime(DateTime expiration)
        {
            uint expires = 0;
            if (expiration > DateTime.Now)
            {
                TimeSpan expireLimit = new TimeSpan(30, 0, 0, 0);
                if (expiration.Subtract(DateTime.Now) > expireLimit)
                {
                    expires = (uint)expireLimit.TotalSeconds;
                }
                else
                {
                    expires = (uint)expiration.Subtract(DateTime.Now).TotalSeconds;
                }
            }
            return expires;
        }

        /// <summary>
        /// Resets the socket connection to the server
        /// </summary>
        protected void Reset()
        {
            //Dispose the old socket
            if (this._connection != null)
            {
                this._connection.Dispose();
            }

            //The Tcp/ConnectionOriented and the Udp/Connectionless enumerator members
            //share the same value internally so they are checked as integers to ensure
            //that each one of the pairs are checked properly.
            switch ((int)this.TransportType)
            {
                case 1:
                    this._connection = new Socket(this.RemoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
                    break;
                case 2:
                    this._connection = new Socket(this.RemoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                    break;
                default:
                    throw new NotSupportedException(StringTable.Get("error_InvalidTransport"));
            }
            this._connection.ReceiveTimeout = this.ReceiveTimeout;
            this._connection.SendTimeout = this.SendTimeout;
            this._connection.Connect(this.RemoteEndPoint);
        }

        /// <summary>
        /// Sends a command to the server
        /// </summary>
        /// <param name="command">The command to be sent to the server</param>
        /// <returns>The server response as an array of bytes, or null if no response is required</returns>
        protected byte[] SendCommand(byte[] command)
        {
            if (command == null)
            {
                throw new ArgumentNullException("command");
            }

            byte[] response = null;

            using (MemoryStream output = new MemoryStream())
            {
                try
                {
                    if (this._connection == null || !this._connection.Connected)
                    {
                        this.Reset();
                    }

                    this._connection.Send(command, SocketFlags.None);
                    byte[] receiveBuffer = new byte[1024];
                    //DO NOT place a check around this for available data
                    //If any kind of network lag is present then the available
                    //will be zero because the server was not given sufficient
                    //time to reply.
                    int bytesRead = this._connection.Receive(receiveBuffer);
                    if (bytesRead > 0)
                    {
                        output.Write(receiveBuffer, 0, bytesRead);
                        while (this._connection.Available > 0)
                        {
                            bytesRead = this._connection.Receive(receiveBuffer, SocketFlags.None);
                            output.Write(receiveBuffer, 0, bytesRead);
                        }
                    }
                    response = output.ToArray();
                }
                catch (SocketException)
                {
                    throw;
                }
                catch (Exception)
                {
                    throw;
                }
            }
            return response;
        }

        /// <summary>
        /// Sends a batch of commands to the server
        /// </summary>
        /// <param name="commandBatch">A collection of commands to be sent to the server</param>
        /// <returns>A collection of server responses</returns>
        protected IEnumerable<byte[]> SendCommandBatch(IEnumerable<byte[]> commandBatch)
        {
            if (commandBatch == null)
            {
                throw new ArgumentNullException("commandBatch");
            }
            List<byte[]> responses = new List<byte[]>();
            foreach (byte[] command in commandBatch)
            {
                responses.Add(this.SendCommand(command));
            }
            return responses;
        }
    }
}
